﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.OAuth;
using SimpleInjector;
using Sustainalytics.Auth.Common;
using Sustainalytics.Auth.Service.GA;
using Sustainalytics.Auth.Service.Properties;
using Sustainalytics.Entities;

namespace Sustainalytics.Auth.Service.OAuth
{
    /// <summary>
    /// Since this class is only instantated once at startup
    /// and we need to get new instaces for mongo collections based on the database switch between A/B
    /// we inject the container and use it as a service locator (even if this is an anti-pattern in different contexts)
    /// </summary>
    public class CustomAuthorizationServerProvider : OAuthAuthorizationServerProvider
    {
        private readonly Container _container;

        public CustomAuthorizationServerProvider(Container container)
        {
            _container = container;
        }

        // Called to validate that the origin of the request is a registered "client_id"
        public override async Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
        {

            SetClientId(context);

            var validator = await GetClientValidatorAsync(context);
            if (!validator.IsValid)
            {
                context.SetError(OwinContextData.InvalidClient, validator.ErrorDescription);
                return;
            }

            context.Validated(context.ClientId);
        }

        // Called when a request to the Token endpoint arrives with a "grant_type" of "password"
        public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
        {
            var user = await _container.GetInstance<GaUserRepository>().GetByUsernameAsync(context.UserName);

            var validator = GetResourceOwnerValidator(context.UserName, context.Password, user);
            if (!validator.IsValid)
            {
                context.SetError(validator.Error, validator.ErrorDescription);
                return;
            }

            var claims = await user.GetClaimsAsync(_container.GetInstance<GaClaimsProvider>());

            context.Validated(new ClaimsIdentity(claims, context.Options.AuthenticationType));
        }

        // Called when a request to the Token endpoint arrives with a "grant_type" of "refresh_token"
        public override async Task GrantRefreshToken(OAuthGrantRefreshTokenContext context)
        {
            var username = context.Ticket.Identity.Claims.GetUsername();
            var user = await _container.GetInstance<GaUserRepository>().GetByUsernameAsync(username);
            var validator = GetRefreshTokenValidator(user);

            if (!validator.IsValid)
            {
                context.SetError(validator.Error, validator.ErrorDescription);
                return;
            }

            var claims = await user.GetClaimsAsync(_container.GetInstance<GaClaimsProvider>());
            var newIdentity = new ClaimsIdentity(claims, context.Options.AuthenticationType);
            var newTicket = new AuthenticationTicket(newIdentity, context.Ticket.Properties);

            context.Validated(newTicket);
        }

        private static void SetClientId(OAuthValidateClientAuthenticationContext context)
        {
            string clientId, clientSecret;

            if (!context.TryGetFormCredentials(out clientId, out clientSecret))
            {
                context.TryGetBasicCredentials(out clientId, out clientSecret);
            }
        }

        private Task<Validator> GetClientValidatorAsync(OAuthValidateClientAuthenticationContext context)
        {
            return Task.FromResult(new Validator()
                .When(() => string.IsNullOrEmpty(context.ClientId), Resources.MissingClientId)
                .When(() => context.ClientId != "clientId1", Resources.InvalidClientId));
        }

        private static Validator GetResourceOwnerValidator(string username, string password, GaUser user)
        {
            return new Validator()
                .When(() => string.IsNullOrEmpty(username), Resources.WrongEmail, Resources.MissingUsername)
                .When(() => string.IsNullOrEmpty(password), Resources.UsernamePasswordMismatch, Resources.MissingPassword)
                .When(() => user == null, Resources.WrongEmail, Resources.UserNotFound)
                .When(() => user.Status != Status.Active, Resources.InactiveAccount, Resources.InactiveAccountMessage)
                .When(() => user.PasswordExpirationDate < DateTime.UtcNow, Resources.ExpiredPassword, Resources.ExpiredPasswordMessage)
                .When(() => !PasswordManager.VerifyHashedPassword(user.Password, password), Resources.InvalidCredentials, Resources.InvalidCredentialsMessage)
                .When(() => !user.IsLegalAgreementAccepted, GetLegalAgreementError(user), Resources.LegalAgreementWarning);
        }

        private static Validator GetRefreshTokenValidator(GaUser user)
        {
            return new Validator()
                .When(() => user == null, Resources.WrongEmail, Resources.UserNotFound)
                .When(() => user.Status != Status.Active, Resources.InactiveAccount, Resources.InactiveAccountMessage)
                .When(() => !user.IsLegalAgreementAccepted, GetLegalAgreementError(user), Resources.LegalAgreementWarning);
        }

        private static string GetLegalAgreementError(GaUser user)
            => user != null && user.RelationshipType == RelationshipType.Prospect
                ? Resources.ProspectLegalAgreementNotAccepted
                : Resources.ClientLegalAgreementNotAccepted;
    }
}
